package csvchk

import (
	"encoding/csv"
	"fmt"
	"io"
	"sort"
	"strconv"
	"strings"
)

// Проверяет поток данных формата CSV на соответствие структуре.
// Параметры:
// - csvstruct - структура потока данных (например "i2*;b;i4;f;n(3,2);d;c(10)");
// - comma     - разделитель полей в потоке данных;
// - header    - истина, если первая строка содержит заголовки полей (пропуск);
// - trim      - истина, если требуется триммировать значения;
// - r         - поток входных данных для проверки;
// - w         - поток выходных данных, прошедших проверку;
// - e         - поток вывод сводного отчёта об ошибках.
// Строки соответсвующие структуре выводятся по мере прохождения проверки,
// отчёт об ошибках (если есть) выводится в завершении работы функции.
func Check(csvstruct string, comma rune, header bool, trim bool,
	r io.Reader, w io.Writer, e io.Writer) {

	csvstrs := parseStucture(csvstruct)

	csvr := csv.NewReader(r)
	csvw := csv.NewWriter(w)

	csvr.Comma = comma
	csvr.FieldsPerRecord = len(csvstrs)
	csvw.Comma = comma

	// Если указано, что первая строка содержит заголовки полей,
	//  то она выводится в поток выходных данных без проверки.
	if header {
		record, err := csvr.Read()
		if err == nil {
			if err := csvw.Write(record); err != nil {
				panic(err)
			}
		}
	}
	dterrs := make(map[string]*dterr)
	for { // цикл по строкам данных (пока не закончатся)
		record, err := csvr.Read()
		if err != nil {
			if err == io.EOF {
				break // входные данные закончились, выход из цикла
			}
			// Любая ошибка чтения строки данных считается ошибкой формата.
			de_key := "-1|FRMT"
			if de, ok := dterrs[de_key]; ok {
				de.err_count++
			} else {
				dterrs[de_key] = &dterr{
					fld_index: -1,
					fld_type:  "",
					err_type:  "FRMT",
					err_value: "",
					err_line:  err.Error(),
					err_count: 1,
				}
			}
			// При ошибке формата нет смысла в провеке отдельных значений,
			//  переход к следующей строке из потока входных данных.
			continue
		}

		// Проверка значений полей строки
		eor := false
		for i, s := range record {
			// Триммированаие значения (если нужно и если включено).
			if trim && len(s) > 0 && (s[0] == ' ' || s[len(s)-1:] == " ") {
				s = strings.TrimSpace(s)
				record[i] = s
			}
			// Передача значения функции проверки типа данных
			//  и регистрация ошибки (если есть).
			if is_corr, err_type := csvstrs[i].check(s); !is_corr {
				eor = true
				de_key := strconv.Itoa(i) + "|" + err_type
				if er, ok := dterrs[de_key]; ok {
					er.err_count++
				} else {
					dterrs[de_key] = &dterr{
						fld_index: i,
						fld_type:  csvstrs[i].field,
						err_type:  err_type,
						err_value: stringForPrint(s),
						err_line:  recordForPrint(record),
						err_count: 1,
					}
				}
			}
		}
		if !eor {
			// Строка не содержит ошибок, вывод
			if err := csvw.Write(record); err != nil {
				panic(err)
			}
		}

	}
	csvw.Flush()

	// Вывод сводного отчёт об ошибках (если есть)
	if len(dterrs) > 0 {
		printDataErrors(e, dterrs)
	}
}

// Отчёт об ошибке в строке данных.
type dterr struct {
	fld_index int    // номер поля в исходных данных (начиная с 0)
	fld_type  string // тип данных поля
	err_type  string // тип ошибки (см. далее)
	err_value string // пример ошибочного значения
	err_line  string // пример строки, содержащей ошибку
	err_count int    // количество ошибок данного типа в данном поле
}

// Формирует строку отчёта об ошибке в данных (разделитель - TAB).
func (d *dterr) String() string {
	return fmt.Sprintf(
		"%v\t%v\t%v\t%v\t%v\t%v",
		d.fld_index,
		d.fld_type,
		d.err_type,
		d.err_value,
		d.err_line,
		d.err_count)
}

// Выводит сводный отчёт в поток ошибок.
func printDataErrors(data_err io.Writer, dterrs map[string]*dterr) {
	keys := make([]string, 0, len(dterrs))
	for k := range dterrs {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		fmt.Fprintln(data_err, dterrs[k])
	}
}

// Приводит строку в формат для включения в отчёт ошибок.
// (разделители полей отображаются символом "¦" код "\u00A6").
func recordForPrint(record []string) string {
	var res = make([]string, 0, len(record))
	for _, s := range record {
		res = append(res, stringForPrint(s))
	}
	return strings.Join(res, "\u00A6")
}

// Подготавливает значение к выводу в отчёт ошибок.
// Для наглядного отображения специальные и непечатные символы преобразуются:
//   - пробел заменяется на символ точки "·" (\u00B7);
//   - символы табуляции, возврата каретки и переноса строки на "\t", "\r" и "\n";
//   - символ экранирования (обратный слэш, "\") повторяется дважды ("\\");
//   - символы за пределами печатных (отображаемых) знаков
//     заменяются кодом формата "\u####", где "####" код символа.
func stringForPrint(text string) string {
	var res = make([]rune, 0, len(text))
	for _, r := range text {
		switch r {
		case ' ':
			res = append(res, '\u00B7')
		case '\t':
			res = append(res, []rune("\\t")...)
		case '\r':
			res = append(res, []rune("\\r")...)
		case '\n':
			res = append(res, []rune("\\n")...)
		case '\\':
			res = append(res, []rune("\\\\")...)
		default:
			if r < ' ' || r == '\u00A6' || r == '\u00B7' {
				ustr := fmt.Sprintf("\\u%04X", r)
				res = append(res, []rune(ustr)...)
			} else {
				res = append(res, r)
			}
		}
		if len(res) > 990 {
			break
		}
	}
	return string(res)
}
